// Package goclient k8s apiServer 访问服务
package goclient

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/restmapper"
	"k8s.io/client-go/tools/clientcmd"
)

const (
	kubeConfigFilePath = "/.kube/config" // 系统用户访问apiServer的配置文件
	NAMESPACE          = "default"       // 访问的namespace
	NODEIP             = "localhost"
)

var SvcNotExists = errors.New("service not exists")

// 对应k8s资源编号，按需添加
// string中注意开头大写
const (
	SOURCE_TYPE_POD = 1 // Pod
	SOURCE_TYPE_DEP = 2 // Deployment
	SOURCE_TYPE_SVC = 3 // Service
)

type ResTracker struct {
	Name      string
	Namespace string
	ResType   int
}

// 自定义err输出
type typeErr struct {
	Msg string
}

func (err typeErr) Error() string {
	return err.Msg
}

// resType string 转 编号；
// 如果类型不存在返回-1
func getTypeNum(resType string) int {
	switch resType {
	case "Pod":
		return SOURCE_TYPE_POD
	case "Deployment":
		return SOURCE_TYPE_DEP
	case "Service":
		return SOURCE_TYPE_SVC
	}

	return -1
}

// resType 编号 转 string；
// 如果类型不存在返回空字符串
func getTypeString(resType int) string {
	switch resType {
	case SOURCE_TYPE_POD:
		return "Pod"
	case SOURCE_TYPE_DEP:
		return "Deployment"
	case SOURCE_TYPE_SVC:
		return "Service"
	}

	return ""
}

// 获取clientset
func initClientSet(inCluster bool) (clientSet *kubernetes.Clientset, err error) {
	var config *rest.Config
	home, _ := os.UserHomeDir()
	if inCluster {
		// 集群中的配置加载方式
		config, err = rest.InClusterConfig()
		if err != nil {
			return nil, err
		}
	} else {
		// 构造访问config的配置，从文件中加载
		config, err = clientcmd.BuildConfigFromFlags("", home+kubeConfigFilePath)
		if err != nil {
			return nil, err
		}
	}

	// 创建clientset
	clientSet, err = kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	return clientSet, nil
}

// 获取dynamic clientset
func initDynamicClient(inCluster bool) (dynamicClient *dynamic.DynamicClient, err error) {
	var config *rest.Config
	home, _ := os.UserHomeDir()
	if inCluster {
		// 集群中的配置加载方式
		config, err = rest.InClusterConfig()
		if err != nil {
			return nil, err
		}
	} else {
		// 构造访问config的配置，从文件中加载
		config, err = clientcmd.BuildConfigFromFlags("", home+kubeConfigFilePath)
		if err != nil {
			return nil, err
		}
	}
	dynamicClient, err = dynamic.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	return dynamicClient, nil
}

type UnstructOption func(*unstructured.Unstructured) error

// WithUidAdd 为metadata下的name增加"-{uid}"信息
func WithUidAdd(uid string) UnstructOption {
	return func(unstruct *unstructured.Unstructured) error {
		if md, ok := unstruct.Object["metadata"]; ok {
			// 获取metadata
			metadata := md.(map[string]interface{})
			// 添加hid信息到name
			metadata["name"] = metadata["name"].(string) + "-" + uid
			return nil
		} else {
			errMsg := typeErr{Msg: "No Metadata"}
			return errMsg
		}
	}
}

type Env struct {
	Key   string
	Value string
}

// WithEnvAdd 只对Deployment类型的资源生效
func WithEnvAdd(env ...Env) UnstructOption {
	return func(unstruct *unstructured.Unstructured) error {

		if kind, ok := unstruct.Object["kind"]; ok {
			kindString := kind.(string)
			if kindString != "Deployment" {
				return nil
			}
		} else {
			return errors.New("kind字段不存在")
		}
		spec, found, err := unstructured.NestedMap(unstruct.Object, "spec")
		if !found {
			return errors.New("未找到spec字段")
		}
		if err != nil {
			return err
		}
		containers, found, err := unstructured.NestedSlice(spec, "template", "spec", "containers")
		if !found {
			return errors.New("未找到containers字段或containers字段位置有误")
		}
		if err != nil {
			return err
		}

		// 对于cybertwin的deployment来说，这里只有一个
		if len(containers) == 0 {
			return errors.New("containers长度为0")
		}

		if container, ok := containers[0].(map[string]interface{}); ok {
			envSlice, found, _ := unstructured.NestedSlice(container, "env")
			if !found {
				envSlice = make([]interface{}, 0)
			}
			for _, each := range env {
				m := map[string]interface{}{
					"name":  each.Key,
					"value": each.Value,
				}
				envSlice = append(envSlice, m)
			}
			_ = unstructured.SetNestedSlice(container, envSlice, "env")
			containers[0] = container // 注意，前面那个if赋值中的类型转换的赋值是深拷贝，因此这里需要重新写会containers[0]上
		}
		// 将更新后的containers更新回spec中
		err = unstructured.SetNestedSlice(spec, containers, "template", "spec", "containers")
		if err != nil {
			return err
		}
		// 将更新后的spec写如unstruct中
		err = unstructured.SetNestedMap(unstruct.Object, spec, "spec")
		if err != nil {
			return err
		}

		return nil
	}
}

// CreateByYAML 根据YAML创建资源对象，dynamic client可以适配所有资源；
// 若为multi-yaml，其中一个资源创建失败则会回滚删除同文件成功创建的其他资源；
// res为-1表示创建失败；为-2表示创建失败且有回滚失败，需要手动删除资源；
// res为1表示没有err
//
// ---------参数说明---------
// file: 创建调用的yaml文件目录，如果在镜像中需要挂载进入镜像
// inCluster: 是否部署到集群中，false为在系统用户状态下调用，需要通过~/.kube/config的文件获取证书，
// opts：对yaml文件做的改动
// true为部署到集群后的调用，接口会自动调用集群挂载的证书地址
func CreateByYAML(file string, inCluster bool, opts ...UnstructOption) (res int, err error) {
	res = 1

	// 创建clientset
	client, err := initClientSet(inCluster)
	if err != nil {
		return -1, err
	}

	// resTracker 基本信息
	nameSpace := "default"
	name := ""
	resType := -1
	resTypeStr := ""

	f, err := os.Open(file)
	if err != nil {
		return -1, err
	}

	// 解析yaml文件
	d := yaml.NewYAMLOrJSONDecoder(f, 4096)
	dc := client.Discovery()

	restMapperRes, err := restmapper.GetAPIGroupResources(dc)
	if err != nil {
		return -1, err
	}

	restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)

	// 构建创建记录，以便回滚
	var resList []ResTracker

	// 循环读取，可能为multi-yaml
	for {
		ext := runtime.RawExtension{}
		if errMsg := d.Decode(&ext); errMsg != nil {
			if errMsg == io.EOF {
				break
			}
			res = -1
			err = errMsg
			break
		}

		// runtime.Object
		obj, gvk, errMsg := unstructured.UnstructuredJSONScheme.Decode(ext.Raw, nil, nil)
		if errMsg != nil {
			err = errMsg
			res = -1
			break
		}

		// GVK获取GVR
		mapping, errMsg := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if errMsg != nil {
			err = errMsg
			res = -1
			break
		}

		// runtime.Object转换为unstructed
		unstructuredObj, errMsg := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
		if errMsg != nil {
			res = -1
			err = errMsg
			break
		}
		// fmt.Printf("unstructuredObj: %+v", unstructuredObj)

		var unstruct unstructured.Unstructured

		unstruct.Object = unstructuredObj

		for _, opt := range opts {
			err = opt(&unstruct)
			if err != nil {
				break
			}
		}
		if err != nil {
			res = -1
			err = errors.New("option运行失败")
			break
		}

		if md, ok := unstruct.Object["kind"]; ok {
			resTypeStr = md.(string)
			resType = getTypeNum(resTypeStr)
		} else {
			res = -1
			err = typeErr{Msg: "No Kind "}
			break
		}

		// 检测resource是否为预定义类型
		if resType == -1 {
			res = -1
			err = typeErr{Msg: "Invalid Resource Type: " + resTypeStr}
			break
		}

		// 获取metadata标签下的name和namespace，以供后续检查资源是否已经存在
		if md, ok := unstruct.Object["metadata"]; ok {
			metadata := md.(map[string]interface{})
			if internalNS, ok := metadata["namespace"]; ok {
				nameSpace = internalNS.(string)
			}
			if internalName, ok := metadata["name"]; ok {
				name = internalName.(string)
			}
		}

		// check是否已经存在该资源
		check, errMsg := checkResource(client, name, nameSpace, resType)
		if errMsg != nil {
			res = -1
			err = errMsg
			break
		}

		// 不存在，创建资源
		if !check {

			// 获取动态客户端
			dclient, errMsg := initDynamicClient(inCluster)
			if errMsg != nil {
				res = -1
				err = errMsg
				break
			}

			// 动态客户端调用create创建资源
			_, errMsg = dclient.Resource(mapping.Resource).Namespace(nameSpace).Create(context.TODO(), &unstruct, metav1.CreateOptions{})
			if errMsg != nil {
				res = -1
				err = errMsg
				break
			}
		}
		// 创建成功或已存在，resList进行记录
		resList = append(resList, ResTracker{Name: name, Namespace: nameSpace, ResType: resType})
	}

	// 如果有出错，multi-yaml中已创建的部分进行回滚删除
	// 如果回滚过程删除失败或删除不完整，res返回-2，err仍为前文报错
	if res < 0 {
		// fmt.Println("resList Rollback")
		for _, obj := range resList {
			// fmt.Printf("Name:%v, Namespace:%v, Type: %v\n", obj.Name, obj.Namespace, getTypeString(obj.ResType))
			// 统一删除接口
			errMsg := deleteResource(client, obj.Name, obj.Namespace, obj.ResType)
			if errMsg != nil {
				res = -2
				break
			}
		}
		return res, err
	}

	return res, nil
}

// GetSvcPort 根据服务名获取svc的nodeport，如果非nodeport返回0 + nil，查询为空返回-1 + nil
//
// ---------参数说明---------
// name: 查询的服务名称
// namespace: 查询的命名空间
// inCluster: 是否部署到集群中，false为在系统用户状态下调用，需要通过~/.kube/config的文件获取证书；
// true为部署到集群后的调用，接口会自动调用集群挂载的证书地址
func GetSvcPort(name, nameSpace string, inCluster bool) (port int32, err error) {
	port = -1

	client, err := initClientSet(inCluster)
	if err != nil {
		return port, err
	}

	// GET获取svc信息
	pods, err := client.CoreV1().Services(nameSpace).Get(context.TODO(), name, metav1.GetOptions{})
	checkStr := "services \"" + name + "\" not found"
	if err != nil {
		if checkStr == fmt.Sprint(err) {
			return port, SvcNotExists
		} else {
			return port, err
		}
	}
	if pods != nil {
		// 不为NodePort则查询返回0
		port = pods.Spec.Ports[0].NodePort
	}

	return port, nil
}

// 删除接口，根据资源name、namespace、type进行删除；
// 复用deleteSource接口
func DeleteResource(name, nameSpace, resType string, inCluster bool) (err error) {
	client, err := initClientSet(inCluster)
	if err != nil {
		return err
	}

	err = deleteResource(client, name, nameSpace, getTypeNum(resType))
	if err != nil {
		return err
	}
	return nil
}

// delete接口，根据资源name、namespace、type进行删除;
// 资源种类不符合预设会返回invalid的err
//
// ---------参数说明---------
// inCluster: 是否部署到集群中，false为在系统用户状态下调用，需要通过~/.kube/config的文件获取证书
// resType：定义的类型编号，可以通过接口从string转换
func deleteResource(clientSet *kubernetes.Clientset, name string, nameSpace string, resType int) (err error) {
	switch resType {
	case SOURCE_TYPE_POD:
		err = clientSet.CoreV1().Pods(nameSpace).Delete(context.TODO(), name, metav1.DeleteOptions{})
	case SOURCE_TYPE_DEP:
		err = clientSet.AppsV1().Deployments(nameSpace).Delete(context.TODO(), name, metav1.DeleteOptions{})
	case SOURCE_TYPE_SVC:
		err = clientSet.CoreV1().Services(nameSpace).Delete(context.TODO(), name, metav1.DeleteOptions{})
	default:
		err = typeErr{Msg: "Invalid Resource Type"}
	}
	if err != nil {
		return err
	}

	return nil
}

// check接口，检查资源是否存在
// 存在返回true + nil，不存在返回false + nil
func checkResource(clientSet *kubernetes.Clientset, name string, nameSpace string, resType int) (exist bool, err error) {
	switch resType {
	case SOURCE_TYPE_POD:
		pod, errMsg := clientSet.CoreV1().Pods(nameSpace).Get(context.TODO(), name, metav1.GetOptions{})
		checkStr := "pods \"" + name + "\" not found"
		if errMsg != nil {
			if checkStr != fmt.Sprint(errMsg) {
				err = errMsg
				break
			} else {
				return false, nil
			}
		}
		if pod != nil {
			return true, nil
		}
	case SOURCE_TYPE_DEP:
		dep, errMsg := clientSet.AppsV1().Deployments(nameSpace).Get(context.TODO(), name, metav1.GetOptions{})
		checkStr := "deployments.apps \"" + name + "\" not found"
		if errMsg != nil {
			if checkStr != fmt.Sprint(errMsg) {
				err = errMsg
				break
			} else {
				return false, nil
			}
		}
		if dep != nil {
			return true, nil
		}
	case SOURCE_TYPE_SVC:
		svc, errMsg := clientSet.CoreV1().Services(nameSpace).Get(context.TODO(), name, metav1.GetOptions{})
		checkStr := "services \"" + name + "\" not found"
		if errMsg != nil {
			if checkStr != fmt.Sprint(errMsg) {
				err = errMsg
				break
			} else {
				return false, nil
			}
		}
		if svc != nil {
			return true, nil
		}
	default:
		err = typeErr{Msg: "Invalid Resource Type"}
	}

	return false, err
}
